// Copyright 2016 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package grpcutil

import (
	"context"
	"fmt"
	"math"
	"os"
	"strconv"
	"strings"

	"github.com/cockroachdb/cockroach/pkg/util/log"
	"github.com/cockroachdb/cockroach/pkg/util/log/severity"
	"google.golang.org/grpc/grpclog"
)

func init() {
	// gRPC's logs are not very useful outside of debugging situations.
	// vmodule works for them, with special semantics as follows:
	// 1: log error and fatal only;
	// 2: log warning, error, fatal;
	// 3: log everything.
	//
	// For example, `--vmodule=clientconn=2` logs everything except INFO.
	LowerSeverity(severity.ERROR)
}

// LowerSeverity ensures that the severity level below which GRPC
// log messages are suppressed is at most the specified severity.
//
// Prior to the first call to this method, this cutoff is ERROR,
// or whatever is specified via the GRPC_GO_LOG_SEVERITY_LEVEL
// environment variable.
//
// Must be called before GRPC is used.
func LowerSeverity(s log.Severity) {
	logger := getGRPCLogger(
		s,
		os.Getenv("GRPC_GO_LOG_SEVERITY_LEVEL"),
		os.Getenv("GRPC_GO_LOG_VERBOSITY_LEVEL"),
	)

	grpclog.SetLoggerV2(logger)
}

func getGRPCLogger(inputSeverity log.Severity, envSev, envVer string) *grpcLogger {
	{
		// If the env var specifies a lower severity than the input,
		// lower the input accordingly.
		var s log.Severity
		switch e := strings.ToLower(envSev); e {
		case "info":
			s = severity.INFO
		case "warning":
			s = severity.WARNING
		case "error":
			s = severity.ERROR
		case "":
			s = inputSeverity
		default:
			panic("unsupported GRPC_GO_LOG_SEVERITY_LEVEL: " + e)
		}
		if s < inputSeverity {
			inputSeverity = s
		}
	}

	var vl int
	if envVer != "" {
		vli, err := strconv.ParseInt(envVer, 10, 32)
		if err != nil {
			panic("invalid GRPC_GO_LOG_VERBOSITY_LEVEL " + envVer + ": " + err.Error())
		}
		if vl < 0 {
			vli = 0
		}
		if vli > math.MaxInt32 {
			vli = math.MaxInt32
		}
		vl = int(vli)
	}

	return &grpcLogger{
		sev: inputSeverity, grpcVerbosityLevel: vl,
	}
}

// NB: This interface is implemented by a pointer because using a value causes
// a synthetic stack frame to be inserted on calls to the interface methods.
// Specifically, we get a stack frame that appears as "<autogenerated>", which
// is not useful in logs.
var _ grpclog.LoggerV2 = (*grpcLogger)(nil)
var _ grpclog.DepthLoggerV2 = (*grpcLogger)(nil)

type grpcLogger struct {
	sev                log.Severity
	grpcVerbosityLevel int
}

// gRPC doesn't consistently propagate all of its stack frame increments. At
// time of writing it's missing the last one. Easy to see by setting a
// breakpoint on WarningDepth and looking at a stack from TestHeartbeatDialback.
// We add once more to account for the stack frame we're introducing.
const depthLoggerDelta = 2

func (l *grpcLogger) sanitize(args []interface{}) []interface{} {
	// NB: redaction would be nice, but the gRPC log args are actually just
	// a subsystem like "[core]" followed by a free-form message that often
	// has a multi-line JSON object in it (but not in any structured way),
	// so there's really no way to work with it. Best we can do is throw
	// away the many newlines.
	sl := make([]interface{}, 0, len(args))
	for _, arg := range args {
		sl = append(sl, strings.Replace(fmt.Sprint(arg), "\n", " ", -1))
	}
	return sl
}

func (l *grpcLogger) InfoDepth(depth int, args ...interface{}) {
	depth += depthLoggerDelta
	if !l.shouldLog(severity.INFO, depth) {
		return
	}
	log.InfofDepth(context.TODO(), depth, "", l.sanitize(args)...)
}

func (l *grpcLogger) WarningDepth(depth int, args ...interface{}) {
	depth += depthLoggerDelta
	if !l.shouldLog(severity.WARNING, depth) {
		return
	}
	log.WarningfDepth(context.TODO(), depth, "", l.sanitize(args)...)
}

func (l *grpcLogger) ErrorDepth(depth int, args ...interface{}) {
	depth += depthLoggerDelta
	if !l.shouldLog(severity.ERROR, depth) {
		return
	}
	log.ErrorfDepth(context.TODO(), depth, "", l.sanitize(args)...)
}

func (l *grpcLogger) FatalDepth(depth int, args ...interface{}) {
	depth += depthLoggerDelta
	// Never suppress fatals.
	log.FatalfDepth(context.TODO(), depth, "", l.sanitize(args)...)
}

func (l *grpcLogger) vDepth(i int, depth int) bool {
	if i < 0 {
		i = 0
	}
	if i > math.MaxInt32 {
		i = math.MaxInt32
	}

	// If GRPC_GO_LOG_VERBOSITY_LEVEL is less restrictive, it prevails.
	if i <= l.grpcVerbosityLevel {
		return true
	}
	// Otherwise, our logger decides.
	return log.VDepth(log.Level(i) /* level */, depth+1)
}

func (l *grpcLogger) shouldLog(incomingSeverity log.Severity, depth int) bool {
	// If the incoming severity is at or above threshold, log.
	if l.sev <= incomingSeverity {
		return true
	}
	// If verbose logging is on (either for our
	// logger or via the grpc env var), log all severities.
	i := 4 - int(incomingSeverity)
	if i < 0 {
		i = 0 // shouldn't happen
	}
	return l.vDepth(i, depth+1)
}
